Õppige, kuidas ehitada JavaScriptis asünkroonsete iteraatoritega suure läbilaskevõimega paralleelprotsessor. Meisterdage samaaegne voohaldus, et andmemahukaid rakendusi oluliselt kiirendada.
Kõrgjõudlusega JavaScripti potentsiaali vallandamine: Põhjalik ülevaade iteraatori abiliste paralleelprotsessoritest samaaegseks voohalduseks
Tänapäeva tarkvaraarenduse maailmas ei ole jõudlus mitte funktsioon, vaid fundamentaalne nõue. Alates suurte andmekogumite töötlemisest taustateenuses kuni keerukate API-interaktsioonide käsitlemiseni veebirakenduses on võime asünkroonseid operatsioone tõhusalt hallata esmatähtis. JavaScript on oma ühelõimelise, sündmuspõhise mudeliga pikka aega silma paistnud I/O-põhiste ülesannetega. Kuid andmemahtude kasvades muutuvad traditsioonilised järjestikused töötlemismeetodid olulisteks kitsaskohtadeks.
Kujutage ette, et peate hankima 10 000 toote üksikasjad, töötlema gigabaidisuurust logifaili või genereerima pisipilte sadadele kasutajate üles laaditud piltidele. Nende ülesannete ükshaaval käsitlemine on usaldusväärne, kuid piinavalt aeglane. Võti dramaatilise jõudluse kasvu saavutamiseks peitub konkurentsuses—mitme elemendi samaaegses töötlemises. See on koht, kus asünkroonsete iteraatorite võimsus koos kohandatud paralleeltöötluse strateegiaga muudab meie andmevoogude käsitlemise viisi.
See põhjalik juhend on mõeldud kesktasemel ja edasijõudnud JavaScripti arendajatele, kes soovivad liikuda kaugemale lihtsatest `async/await` tsüklitest. Uurime JavaScripti iteraatorite aluseid, süveneme järjestikuste kitsaskohtade probleemi ja, mis kõige tähtsam, ehitame nullist võimsa, taaskasutatava iteraatori abilise paralleelprotsessori. See tööriist võimaldab teil hallata samaaegseid ülesandeid mis tahes andmevoo puhul peenhäälestatud kontrolliga, muutes teie rakendused kiiremaks, tõhusamaks ja skaleeritavamaks.
Aluste mõistmine: Iteraatorid ja asünkroonne JavaScript
Enne kui saame oma paralleelprotsessori ehitada, peame omama kindlat arusaama JavaScripti aluskontseptsioonidest, mis selle võimalikuks teevad: iteraatori protokollidest ja nende asünkroonsetest vastetest.
Iteraatorite ja itereeritavate võimsus
Oma tuumas pakub iteraatori protokoll standardset viisi väärtuste jada tootmiseks. Objekti peetakse itereeritavaks, kui see implementeerib meetodi võtmega `Symbol.iterator`. See meetod tagastab iteraatori objekti, millel on `next()` meetod. Iga `next()` kutse tagastab objekti kahe omadusega: `value` (jada järgmine väärtus) ja `done` (tõeväärtus, mis näitab, kas jada on lõppenud).
See protokoll on `for...of` tsükli taga peituv maagia ja on paljudes sisseehitatud tüüpides loomupäraselt implementeeritud:
- Massiivid: `['a', 'b', 'c']`
- Sõned: `"hello"`
- Mapid: `new Map([['key1', 'value1'], ['key2', 'value2']])`
- Setid: `new Set([1, 2, 3])`
Itereeritavate ilu seisneb selles, et nad esindavad andmevoogusid laisalt. Te tõmbate väärtusi ükshaaval, mis on uskumatult mälusäästlik suurte või isegi lõpmatute jadade puhul, kuna te ei pea hoidma kogu andmestikku korraga mälus.
Asünkroonsete iteraatorite esiletõus
Standardne iteraatori protokoll on sünkroonne. Mis siis, kui meie jada väärtused ei ole kohe saadaval? Mis siis, kui need tulevad võrgupäringust, andmebaasikursorist või failivoost? Siin tulevad mängu asünkroonsed iteraatorid.
Asünkroonse iteraatori protokoll on oma sünkroonse vaste lähedane sugulane. Objekt on asünkroonselt itereeritav, kui sellel on meetod võtmega `Symbol.asyncIterator`. See meetod tagastab asünkroonse iteraatori, mille `next()` meetod tagastab `Promise`'i, mis laheneb tuttavaks `{ value, done }` objektiks.
See võimaldab meil töötada andmevoogudega, mis saabuvad aja jooksul, kasutades elegantset `for await...of` tsüklit:
Näide: Asünkroonne generaator, mis väljastab numbreid viivitusega.
async function* createDelayedNumberStream() {
for (let i = 1; i <= 5; i++) {
// Simuleerib võrguviivitust või muud asünkroonset operatsiooni
await new Promise(resolve => setTimeout(resolve, 500));
yield i;
}
}
async function consumeStream() {
const numberStream = createDelayedNumberStream();
console.log('Starting consumption...');
// Tsükkel peatub iga 'await' juures, kuni järgmine väärtus on valmis
for await (const number of numberStream) {
console.log(`Received: ${number}`);
}
console.log('Consumption finished.');
}
// Väljundis ilmuvad numbrid iga 500ms järel
See muster on fundamentaalne kaasaegseks andmetöötluseks Node.js-is ja veebilehitsejates, võimaldades meil suuri andmeallikaid sujuvalt käsitleda.
Iteraatori abiliste ettepaneku tutvustus
Kuigi `for...of` tsüklid on võimsad, võivad need olla imperatiivsed ja paljusõnalised. Massiivide jaoks on meil rikkalik komplekt deklaratiivseid meetodeid nagu `.map()`, `.filter()` ja `.reduce()`. Iteraatori abiliste TC39 ettepanek püüab tuua sama väljendusrikka võimsuse otse iteraatoritele.
See ettepanek lisab meetodid `Iterator.prototype`-le ja `AsyncIterator.prototype`-le, võimaldades meil aheldada operatsioone mis tahes itereeritaval allikal, ilma et peaksime seda esmalt massiiviks teisendama. See on mängumuutev mälutõhususe ja koodi selguse seisukohast.
Vaatleme seda "enne ja pärast" stsenaariumi andmevoo filtreerimiseks ja kaardistamiseks:
Enne (standardse tsükliga):
async function processData(source) {
const results = [];
for await (const item of source) {
if (item.value > 10) { // filterdamine
const processedItem = await transform(item); // kaardistamine
results.push(processedItem);
}
}
return results;
}
Pärast (pakutud asünkroonsete iteraatori abilistega):
async function processDataWithHelpers(source) {
const results = await source
.filter(item => item.value > 10)
.map(async item => await transform(item))
.toArray(); // .toArray() on teine pakutud abiline
return results;
}
Kuigi see ettepanek ei ole veel kõigis keskkondades keele standardne osa, moodustavad selle põhimõtted meie paralleelprotsessori kontseptuaalse aluse. Me tahame luua `map`-laadse operatsiooni, mis ei töötle mitte ainult ühte elementi korraga, vaid käivitab mitu `transform` operatsiooni paralleelselt.
Kitsaskoht: Järjestikune töötlemine asünkroonses maailmas
`for await...of` tsükkel on fantastiline tööriist, kuid sellel on üks oluline omadus: see on järjestikune. Tsükli keha ei alga järgmise elemendi jaoks enne, kui praeguse elemendi `await` operatsioonid on täielikult lõpule viidud. See loob jõudluse lae, kui tegeleda sõltumatute ülesannetega.
Illustreerime seda levinud, reaalse maailma stsenaariumiga: andmete hankimine API-st identifikaatorite loendi jaoks.
Kujutage ette, et meil on asünkroonne iteraator, mis väljastab 100 kasutaja ID-d. Iga ID jaoks peame tegema API-kutse, et saada kasutaja profiil. Oletame, et iga API-kutse võtab keskmiselt 200 millisekundit.
async function fetchUserProfile(userId) {
// Simuleerib API-kutset
await new Promise(resolve => setTimeout(resolve, 200));
return { id: userId, name: `User ${userId}`, fetchedAt: new Date() };
}
async function fetchAllUsersSequentially(userIds) {
console.time('SequentialFetch');
const profiles = [];
for await (const id of userIds) {
const profile = await fetchUserProfile(id);
profiles.push(profile);
console.log(`Fetched user ${id}`);
}
console.timeEnd('SequentialFetch');
return profiles;
}
// Assuming 'userIds' is an async iterable of 100 IDs
// await fetchAllUsersSequentially(userIds);
Mis on kogu täitmisaeg? Kuna iga `await fetchUserProfile(id)` peab lõpule jõudma enne järgmise algust, on kogu aeg ligikaudu:
100 kasutajat * 200 ms/kasutaja = 20 000 ms (20 sekundit)
See on klassikaline I/O-põhine kitsaskoht. Ajal, mil meie JavaScripti protsess ootab võrgu vastust, on selle sündmusteahel enamasti jõude. Me ei kasuta süsteemi ega välise API täit võimsust. Töötlemise ajajoon näeb välja selline:
Ülesanne 1: [---OOTUS---] Valmis
Ülesanne 2: [---OOTUS---] Valmis
Ülesanne 3: [---OOTUS---] Valmis
...ja nii edasi.
Meie eesmärk on muuta see ajajoon millekski selliseks, kasutades konkurentsuse taset 10:
Ülesanne 1-10: [---OOTUS---][---OOTUS---]... Valmis
Ülesanne 11-20: [---OOTUS---][---OOTUS---]... Valmis
...
10 samaaegse operatsiooniga saame teoreetiliselt vähendada kogu aega 20 sekundilt vaid 2 sekundile. See on jõudlushüpe, mille püüame saavutada, ehitades oma paralleelprotsessori.
JavaScripti iteraatori abilise paralleelprotsessori ehitamine
Nüüd jõuame selle artikli tuumani. Konstrueerime taaskasutatava asünkroonse generaatori funktsiooni, mida nimetame `parallelMap`, mis võtab sisendiks asünkroonse itereeritava allika, kaardistamisfunktsiooni ja konkurentsuse taseme. See toodab uue asünkroonse itereeritava, mis väljastab töödeldud tulemusi nende kättesaadavaks muutumisel.
Põhilised disainiprintsiibid
- Konkurentsuse piiramine: Protsessoril ei tohi kunagi olla korraga käimas rohkem kui määratud arv `mapper` funktsiooni promise'e. See on kriitilise tähtsusega ressursside haldamiseks ja väliste API-de kiiruspiirangute austamiseks.
- Laisk tarbimine: See peab tõmbama allika iteraatorist andmeid ainult siis, kui selle töötlemisbasseinis on vaba koht. See tagab, et me ei puhverda kogu allikat mällu, säilitades voogude eelised.
- Vasturõhu käsitlemine: Protsessor peaks loomulikult peatuma, kui selle väljundi tarbija on aeglane. Asünkroonsed generaatorid saavutavad selle automaatselt `yield` võtmesõna kaudu. Kui täitmine on peatatud `yield` juures, ei tõmmata allikast uusi elemente.
- Järjestamata väljund maksimaalse läbilaskevõime jaoks: Suurima võimaliku kiiruse saavutamiseks väljastab meie protsessor tulemused kohe, kui need on valmis, mitte tingimata sisendi algses järjekorras. Järjekorra säilitamist arutame hiljem edasijõudnute teemana.
`parallelMap` implementatsioon
Ehitame oma funktsiooni samm-sammult. Parim tööriist kohandatud asünkroonse iteraatori loomiseks on `async function*` (asünkroonne generaator).
/**
* Loob uue asünkroonse itereeritava, mis töötleb elemente allik-itereeritavast paralleelselt.
* @param {AsyncIterable|Iterable} source Töödeldav allik-itereeritav.
* @param {Function} mapperFn Asünkroonne funktsioon, mis võtab elemendi ja tagastab töödeldud tulemuse promise'i.
* @param {object} options
* @param {number} options.concurrency Maksimaalne arv ülesandeid, mida paralleelselt käivitada.
* @returns {AsyncGenerator} Asünkroonne generaator, mis väljastab töödeldud tulemusi.
*/
async function* parallelMap(source, mapperFn, { concurrency = 5 }) {
// 1. Hangi asünkroonne iteraator allikast.
// See töötab nii sünkroonsete kui ka asünkroonsete itereeritavatega.
const asyncIterator = source[Symbol.asyncIterator] ?
source[Symbol.asyncIterator]() :
source[Symbol.iterator]();
// 2. Set, et hoida arvet hetkel töödeldavate ülesannete promise'ide üle.
// Seti kasutamine muudab promise'ide lisamise ja kustutamise tõhusaks.
const processing = new Set();
// 3. Lipp, et jälgida, kas allika iteraator on ammendatud.
let sourceIsDone = false;
// 4. Peamine tsükkel: jätkub seni, kuni ülesandeid töödeldakse
// või allikal on veel elemente.
while (!sourceIsDone || processing.size > 0) {
// 5. Täida töötlemisbassein kuni konkurentsuse piirini.
while (processing.size < concurrency && !sourceIsDone) {
const nextItemPromise = asyncIterator.next();
const processingPromise = nextItemPromise.then(item => {
if (item.done) {
sourceIsDone = true;
return; // Annab märku, et see haru on lõppenud, töödeldavat tulemust pole.
}
// Käivita kaardistamisfunktsioon ja veendu, et selle tulemus on promise.
// See tagastab lõpliku töödeldud väärtuse.
return Promise.resolve(mapperFn(item.value));
});
// See on basseini haldamisel ülioluline samm.
// Loome ümbris-promise'i, mis lahenedes annab meile nii
// lõpliku tulemuse kui ka viite iseendale, et saaksime selle basseinist eemaldada.
const trackedPromise = processingPromise.then(result => ({
result,
origin: trackedPromise
}));
processing.add(trackedPromise);
}
// 6. Kui bassein on tühi, peame olema lõpetanud. Katkesta tsükkel.
if (processing.size === 0) break;
// 7. Oota, kuni ÜKSKÕIK milline töötlemisülesanne lõpeb.
// Promise.race() on selle saavutamise võti.
const { result, origin } = await Promise.race(processing);
// 8. Eemalda lõpetatud promise töötlemisbasseinist.
processing.delete(origin);
// 9. Väljasta tulemus, välja arvatud juhul, kui see on 'done' signaalist tulenev 'undefined'.
// See peatab generaatori, kuni tarbija küsib järgmist elementi.
if (result !== undefined) {
yield result;
}
}
}
Loogika lahtiseletamine
- Initsialiseerimine: Saame asünkroonse iteraatori allikast ja initsialiseerime `Set`'i nimega `processing`, mis toimib meie konkurentsuse basseinina.
- Basseini täitmine: Sisemine `while`-tsükkel on mootor. See kontrollib, kas `processing` setis on ruumi ja kas `source`-il on veel elemente. Kui jah, siis tõmbab see järgmise elemendi.
- Ülesande täitmine: Iga elemendi jaoks kutsume välja `mapperFn`. Kogu operatsioon — järgmise elemendi hankimine ja selle kaardistamine — on pakitud promise'i (`processingPromise`).
- Promise'ide jälgimine: Kõige keerulisem osa on teada, milline promise pärast `Promise.race()`'i setist eemaldada. `Promise.race()` tagastab lahenenud väärtuse, mitte promise'i objekti ennast. Selle lahendamiseks loome `trackedPromise`, mis laheneb objektiks, mis sisaldab nii lõplikku `result`-i kui ka viidet iseendale (`origin`). Lisame selle jälgimis-promise'i oma `processing` setti.
- Kiireima ülesande ootamine: `await Promise.race(processing)` peatab täitmise, kuni basseini esimene ülesanne lõpeb. See on meie konkurentsuse mudeli süda.
- Väljastamine ja täiendamine: Kui ülesanne lõpeb, saame selle tulemuse. Eemaldame sellele vastava `trackedPromise` `processing` setist, mis vabastab koha. Seejärel `yield`-ime tulemuse. Kui tarbija tsükkel küsib järgmist elementi, jätkab meie peamine `while`-tsükkel ja sisemine `while`-tsükkel püüab tühja koha täita uue ülesandega allikast.
See loob isereguleeruva konveieri. Basseini tühjendatakse pidevalt `Promise.race`'iga ja täidetakse uuesti allika iteraatorist, säilitades stabiilse samaaegsete operatsioonide oleku.
Meie `parallelMap`-i kasutamine
Vaatame uuesti meie kasutajate hankimise näidet ja rakendame oma uut utiliiti.
// Oletame, et 'createIdStream' on asünkroonne generaator, mis väljastab 100 kasutaja ID-d.
const userIdStream = createIdStream();
async function fetchAllUsersInParallel() {
console.time('ParallelFetch');
const profilesStream = parallelMap(userIdStream, fetchUserProfile, { concurrency: 10 });
for await (const profile of profilesStream) {
console.log(`Processed profile for user ${profile.id}`);
}
console.timeEnd('ParallelFetch');
}
// await fetchAllUsersInParallel();
Konkurentsusega 10 on kogu täitmisaeg nüüd ligikaudu 2 sekundit 20 asemel. Oleme saavutanud 10-kordse jõudluse kasvu, lihtsalt mähkides oma voo `parallelMap`-iga. Ilu seisneb selles, et tarbiv kood jääb lihtsaks, loetavaks `for await...of` tsükliks.
Praktilised kasutusjuhud ja globaalsed näited
See muster ei ole mõeldud ainult kasutajaandmete hankimiseks. See on mitmekülgne tööriist, mida saab rakendada paljudele globaalses rakenduste arenduses levinud probleemidele.
Suure läbilaskevõimega API interaktsioonid
Stsenaarium: Finantsteenuste rakendus peab rikastama tehinguandmete voogu. Iga tehingu jaoks peab see kutsuma kahte välist API-d: üks pettuste avastamiseks ja teine valuuta konverteerimiseks. Nendel API-del on kiiruspiirang 100 päringut sekundis.
Lahendus: Kasutage `parallelMap`-i `concurrency` seadistusega `20` või `30`, et töödelda tehingute voogu. `mapperFn` teeks kaks API-kutset kasutades `Promise.all`-i. Konkurentsuse piirang tagab, et saate suure läbilaskevõime, ületamata API kiiruspiiranguid, mis on kriitilise tähtsusega mure iga rakenduse jaoks, mis suhtleb kolmandate osapoolte teenustega.
Suuremahuline andmetöötlus ja ETL (Extract, Transform, Load)
Stsenaarium: Andmeanalüütika platvorm Node.js keskkonnas peab töötlema 5 GB CSV-faili, mis on salvestatud pilvesalvestusse (nagu Amazon S3 või Google Cloud Storage). Iga rida tuleb valideerida, puhastada ja sisestada andmebaasi.
Lahendus: Looge asünkroonne iteraator, mis loeb faili pilvesalvestuse voost ridade kaupa (nt kasutades `stream.Readable` Node.js-is). Suunake see iteraator `parallelMap`-i. `mapperFn` teostab valideerimisloogika ja andmebaasi `INSERT` operatsiooni. `concurrency` saab häälestada vastavalt andmebaasi ühenduste basseini suurusele. See lähenemine väldib 5 GB faili mällu laadimist ja paralleliseerib konveieri aeglase andmebaasi sisestamise osa.
Pildi- ja videotranskodeerimise konveier
Stsenaarium: Globaalne sotsiaalmeedia platvorm võimaldab kasutajatel videoid üles laadida. Iga video tuleb transkodeerida mitmesse resolutsiooni (nt 1080p, 720p, 480p). See on protsessori-intensiivne ülesanne.
Lahendus: Kui kasutaja laadib üles partii videoid, looge video failiteede iteraator. `mapperFn` võib olla asünkroonne funktsioon, mis käivitab alamprotsessi, et käivitada käsurea tööriist nagu `ffmpeg`. `concurrency` tuleks seada masina saadaolevate protsessorituumade arvule (nt `os.cpus().length` Node.js-is), et maksimeerida riistvara kasutust ilma süsteemi üle koormamata.
Edasijõudnud kontseptsioonid ja kaalutlused
Kuigi meie `parallelMap` on võimas, nõuavad reaalsed rakendused sageli rohkem nüansse.
Tugev veahaldus
Mis juhtub, kui üks `mapperFn` kutsetest tagastab vea? Meie praeguses implementatsioonis tagastab `Promise.race` vea, mis põhjustab kogu `parallelMap` generaatori vea viskamise ja lõpetamise. See on "fail-fast" strateegia.
Sageli soovite vastupidavamat konveierit, mis suudab üle elada üksikuid ebaõnnestumisi. Selle saate saavutada oma `mapperFn`-i mähkimisega.
const resilientMapper = async (item) => {
try {
return { status: 'fulfilled', value: await originalMapper(item) };
} catch (error) {
console.error(`Failed to process item ${item.id}:`, error);
return { status: 'rejected', reason: error, item: item };
}
};
const resultsStream = parallelMap(source, resilientMapper, { concurrency: 10 });
for await (const result of resultsStream) {
if (result.status === 'fulfilled') {
// process successful value
} else {
// handle or log the failure
}
}
Järjekorra säilitamine
Meie `parallelMap` väljastab tulemusi segamini, eelistades kiirust. Mõnikord peab väljundi järjekord vastama sisendi järjekorrale. See nõuab teistsugust, keerukamat implementatsiooni, mida sageli nimetatakse `parallelOrderedMap`.
Järjestatud versiooni üldine strateegia on:
- Töötle elemente paralleelselt nagu varem.
- Selle asemel, et tulemusi kohe väljastada, salvestage need puhvrisse või mappi, kasutades võtmena nende algset indeksit.
- Hoidke loendurit järgmise oodatava indeksi jaoks, mis tuleb väljastada.
- Kontrollige tsüklis, kas praeguse oodatava indeksi tulemus on puhvris saadaval. Kui on, väljastage see, suurendage loendurit ja korrake. Kui ei, oodake, kuni rohkem ülesandeid lõpeb.
See lisab puhvri jaoks lisakulu ja mälukasutust, kuid on vajalik järjekorrast sõltuvate töövoogude jaoks.
Vasturõhu selgitus
Tasub korrata ühte selle asünkroonse generaatoripõhise lähenemise elegantsemat omadust: automaatne vasturõhu käsitlemine. Kui meie `parallelMap`-i tarbiv kood on aeglane — näiteks kirjutades iga tulemuse aeglasele kettale või ummistunud võrgupesasse — ei küsi `for await...of` tsükkel järgmist elementi. See põhjustab meie generaatori peatumise `yield result;` real. Peatatud olekus see ei tsükkelda, ei kutsu `Promise.race`-i ja mis kõige tähtsam, ei täida töötlemisbasseini. See nõudluse puudumine levib tagasi algse allika iteraatorini, millest ei loeta. Kogu konveier aeglustub automaatselt, et sobituda oma kõige aeglasema komponendi kiirusega, vältides mälumahu plahvatust liigsest puhverdamisest.
Kokkuvõte ja tulevikuväljavaated
Oleme rännanud JavaScripti iteraatorite aluskontseptsioonidest keeruka ja kõrgjõudlusega paralleeltöötluse utiliidi ehitamiseni. Liikudes järjestikustest `for await...of` tsüklitest hallatud konkurentsele mudelile, oleme demonstreerinud, kuidas saavutada suurusjärgu võrra paremat jõudlust andmemahukate, I/O-põhiste ja protsessori-põhiste ülesannete puhul.
Peamised järeldused on:
- Järjestikune on aeglane: Traditsioonilised asünkroonsed tsüklid on sõltumatute ülesannete jaoks kitsaskoht.
- Konkurentsus on võtmetähtsusega: Elementide paralleelne töötlemine vähendab oluliselt kogu täitmisaega.
- Asünkroonsed generaatorid on ideaalne tööriist: Need pakuvad puhast abstraktsiooni kohandatud itereeritavate loomiseks sisseehitatud toega olulistele funktsioonidele nagu vasturõhk.
- Kontroll on hädavajalik: Hallatud konkurentsuse bassein hoiab ära ressursside ammendumise ja austab väliste süsteemide piiranguid.
Kuna JavaScripti ökosüsteem areneb edasi, muutub Iteraatori abiliste ettepanek tõenäoliselt keele standardseks osaks, pakkudes kindlat ja loomupärast alust voogude manipuleerimiseks. Kuid paralleliseerimise loogika — promise'ide basseini haldamine tööriistaga nagu `Promise.race` — jääb võimsaks, kõrgema taseme mustriks, mida arendajad saavad rakendada spetsiifiliste jõudlusprobleemide lahendamiseks.
Julgustan teid võtma täna ehitatud `parallelMap` funktsiooni ja katsetama seda oma projektides. Tuvastage oma kitsaskohad, olgu need siis API-kutsed, andmebaasioperatsioonid või failitöötlus, ja vaadake, kuidas see samaaegse voohalduse muster saab muuta teie rakendused kiiremaks, tõhusamaks ja valmis andmepõhise maailma nõudmisteks.